package com.ruoyi.common.utils;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.StopWatch;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.SimpleCache;
import cn.hutool.core.util.ReflectUtil;
import com.google.common.base.Splitter;
import com.ruoyi.common.utils.file.FileTypeUtils;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.upyun.ParallelUploader;
import com.upyun.RestManager;
import com.upyun.UpYunUtils;
import okhttp3.Response;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.stream.Collectors;

public class UpyOssUtil {
    private static final Logger log = LoggerFactory.getLogger(UpyOssUtil.class);

    /**
     * 构造对象缓存
     */
    private static final SimpleCache<Class<?>, Object> INSTANCE_CACHE = new SimpleCache<>();


    /**
     * 上传
     *
     * @param upYunConf  又拍云必填配置
     * @param params     又拍云的可选参数(参考：{@link com.upyun.Params})
     * @param biFunction function
     * @return
     */
    private static <T> List<String> upload(UpYunConf upYunConf, Map<String, String> params, Class<T> clazz, BiFunction<T, Map<String, String>, List<String>> biFunction) {
        //校验参数
        check(StringUtils.isNotBlank(upYunConf.getNameSpace()) || StringUtils.isNotBlank(upYunConf.getUserName()) || StringUtils.isNotBlank(upYunConf.getPassword()) || StringUtils.isNotBlank(upYunConf.getAddress())
                , "upYunConf中必填属性可能存在空值");

        //获取对象实例（先从缓存中获取）
        T instance = getInstance(clazz, upYunConf.getNameSpace(), upYunConf.getUserName(), upYunConf.getPassword());
        List<String> urlList = biFunction.apply(instance, params);
        if (CollUtil.isNotEmpty(urlList)) {
            return urlList.stream().map(url -> {
                if (isRewriteUrl(url)) {
                    //得到的URL如：https://v0.api.upyun.com/mall-cert/test/20211215135525.png
                    //需要进行改写：又拍云控制台提供的域名/目录/文件名.文件后缀名 如：http://mall-cert.test.upcdn.net/test/20211215135525.png
                    Object bucketName = ReflectUtil.getFieldValue(instance, "bucketName");
                    //return removeFutileSlash(StringUtils.join(getScheme(upYunConf.getAddress()), StringUtils.substring(url, url.lastIndexOf(bucketName.toString())+bucketName.toString().length())));
                    return StringUtils.substring(url, url.lastIndexOf(bucketName.toString())+bucketName.toString().length());
                }
                return url;
            }).collect(Collectors.toList());
        }
        return urlList;
    }


    /**
     * 上传
     *
     * @param upYunConf     又拍云配置
     * @param uploadDirPath 上传至又拍云的目录路径 (/test   /test/a/)
     * @param data          文件字节数组
     * @param fileName      文件名，包含文件名后缀(.jpg .txt .png)  注：文件名重复，会出现覆盖,如果该字段不传，那么fileSuffix就必填
     * @param fileSuffix    文件名后缀(.jpg .txt .png),如果文件名fileName参数传值了，这个字段可以不传了,优先级fileName高
     * @param params        又拍云的可选参数(参考：{@link com.upyun.Params})
     * @param checkMD5      是否校验md5
     *                      // 设置待上传文件的 Content-MD5 值
     *                      // 如果又拍云服务端收到的文件MD5值与用户设置的不一致，将会报 406 NotAcceptable 错误
     * @return
     */
    public static String upload(UpYunConf upYunConf, String uploadDirPath, byte[] data, String fileName, String fileSuffix, Map<String, String> params, boolean checkMD5) {
        return upload(upYunConf, params, RestManager.class, (restManager, map) -> {
            String url = "";
            try {
                //校验
                check(isDirectory(uploadDirPath), "上传至又拍云的目录路径不合法,请校验");
                check(StringUtils.isNotBlank(fileName) || StringUtils.isNotBlank(fileSuffix), "文件名fileName或者文件名后缀fileSuffix,两个字段必填其中一个");

                String fileNameTemp = fileName;
                if (StringUtils.isBlank(fileNameTemp)) {
                    fileNameTemp = StringUtils.join(IdUtils.fastSimpleUUID(), fileSuffix);
                }
                String uploadPath = uploadDirPath.endsWith("/") ? StringUtils.join(uploadDirPath, fileNameTemp) : StringUtils.join(uploadDirPath, "/", fileNameTemp);
                //是否校验md5
                if (checkMD5) {
                    params.put(RestManager.PARAMS.CONTENT_MD5.getValue(), UpYunUtils.md5(data));
                }
                Response response = restManager.writeFile(uploadPath, data, map);
                if (response.isSuccessful()) {
                    url = response.request().url().toString();
                }
                log.debug("文件上传 message:{}", response.message());
            } catch (Exception e) {
                log.error("文件上传失败:error:{}", e);
            }
            return Collections.singletonList(url);
        }).get(0);
    }


    /**
     * 上传
     *
     * @param bucketName    桶
     * @param userName      用户名
     * @param password      密码，需要MD5加密
     * @param hosts         如:www.baidu.com
     * @param uploadDirPath uploadDirPath 上传至又拍云的目录路径 (/test   /test/a/)
     * @param data          字节数组
     * @param fileSuffix    文件后缀名
     * @param params        又拍云的可选参数(参考：{@link com.upyun.Params})
     * @return
     */
    public static String upload(String bucketName, String userName, String password, String hosts, String uploadDirPath, byte[] data, String fileSuffix, Map<String, String> params) {
        return upload(UpYunConf.buildUpYunConf(bucketName, userName, password, hosts), uploadDirPath, data, null, fileSuffix, params, false);
    }

    /**
     * 上传
     *
     * @param bucketName    桶
     * @param userName      用户名
     * @param password      密码，需要MD5加密
     * @param hosts         如:www.baidu.com
     * @param uploadDirPath 上传至又拍云的目录路径 (/test   /test/a/)
     * @param data          字节数组
     * @param fileSuffix    文件后缀名
     * @return
     */
    public static String upload(String bucketName, String userName, String password, String hosts, String uploadDirPath, byte[] data, String fileSuffix) {
        return upload(UpYunConf.buildUpYunConf(bucketName, userName, password, hosts), uploadDirPath, data, null, fileSuffix, new HashMap<>(), false);
    }


    /**
     * 上传,可以自己设置是否校验md5，如果文件过大，建议设置为false
     *
     * @param upYunConf     又拍云配置
     * @param uploadDirPath 上传至又拍云的目录路径 (/test   /test/a/)
     * @param dataList      文件字节数组
     * @param fileSuffix    文件名后缀(.jpg .txt .png)
     * @param checkMD5      是否校验md5
     *                      // 设置待上传文件的 Content-MD5 值
     *                      // 如果又拍云服务端收到的文件MD5值与用户设置的不一致，将会报 406 NotAcceptable 错误
     * @return
     */
    public static List<String> upload(UpYunConf upYunConf, String uploadDirPath, List<byte[]> dataList, String fileSuffix, boolean checkMD5) {
        return upload(upYunConf, uploadDirPath, dataList, fileSuffix, new HashMap<>(), checkMD5);
    }

    /**
     * 上传 （默认校验md5，如果文件过大，不建议使用该方法）
     *
     * @param upYunConf     又拍云配置
     * @param uploadDirPath 上传至又拍云的目录路径 (/test   /test/a/)
     * @param dataList      文件字节数组
     * @param fileSuffix    文件名后缀(.jpg .txt .png)
     * @return
     */
    public static List<String> upload(UpYunConf upYunConf, String uploadDirPath, List<byte[]> dataList, String fileSuffix) {
        return upload(upYunConf, uploadDirPath, dataList, fileSuffix, new HashMap<>(), false);
    }


    /**
     * 上传
     *
     * @param upYunConf     又拍云配置
     * @param uploadDirPath 上传至又拍云的目录路径 (/test   /test/a/)
     * @param dataList      文件字节数组
     * @param fileSuffix    文件名后缀(.jpg .txt .png)
     * @param params        又拍云的可选参数(参考：{@link com.upyun.Params})
     * @param checkMD5      是否校验md5
     *                      // 设置待上传文件的 Content-MD5 值
     *                      // 如果又拍云服务端收到的文件MD5值与用户设置的不一致，将会报 406 NotAcceptable 错误
     * @return
     */
    public static List<String> upload(UpYunConf upYunConf, String uploadDirPath, List<byte[]> dataList, String fileSuffix, Map<String, String> params, boolean checkMD5) {
        List<String> resultList = new ArrayList<>();
        try {
            List<Callable<String>> tasks = new ArrayList<>();
            for (byte[] bytes : dataList) {
                Callable<String> callable = () -> upload(upYunConf, uploadDirPath, bytes, null, fileSuffix, params, checkMD5);
                tasks.add(callable);
            }
            //提交任务
            resultList.addAll(submit(tasks));
        } catch (Exception ex) {
            log.error("文件上传失败:error:{}", ex);
        }
        return resultList;
    }


    /**
     * 上传 （默认校验md5，如果文件过大，不建议使用该方法，选择可以将md5校验设置为false的方法）
     *
     * @param bucketName 桶
     * @param userName   用户名
     * @param password   密码，需要MD5加密
     * @param hosts      如:www.baidu.com
     * @param uploadPath 上传文件路径(如果上传文件路径:/test,文件名后缀:.jpg,那么就会后台算法生成文件名，得到的路径如:/test/123.jpg;如果上传文件的路径是/test/123.jpg,那么参数fileSuffix可以为空)
     * @param dataList   文件字节数组
     * @param fileSuffix 文件名后缀(.jpg .txt .png)
     * @return
     */
    public static List<String> upload(String bucketName, String userName, String password, String hosts, String uploadPath, List<byte[]> dataList, String fileSuffix) {
        return upload(UpYunConf.buildUpYunConf(bucketName, userName, password, hosts), uploadPath, dataList, fileSuffix, new HashMap<>(), false);
    }

    /**
     * 上传 （默认校验md5，如果文件过大，不建议使用该方法，选择可以将md5校验设置为false的方法）
     *
     * @param bucketName 桶
     * @param userName   用户名
     * @param password   密码，需要MD5加密
     * @param hosts      如:www.baidu.com
     * @param uploadPath 上传文件路径(如果上传文件路径:/test,文件名后缀:.jpg,那么就会后台算法生成文件名，得到的路径如:/test/123.jpg;如果上传文件的路径是/test/123.jpg,那么参数fileSuffix可以为空)
     * @param dataList   文件字节数组
     * @param fileSuffix 文件名后缀(.jpg .txt .png)
     * @param checkMD5   是否校验md5
     *                   // 设置待上传文件的 Content-MD5 值
     *                   // 如果又拍云服务端收到的文件MD5值与用户设置的不一致，将会报 406 NotAcceptable 错误
     * @return
     */
    public static List<String> upload(String bucketName, String userName, String password, String hosts, String uploadPath, List<byte[]> dataList, String fileSuffix, boolean checkMD5) {
        return upload(UpYunConf.buildUpYunConf(bucketName, userName, password, hosts), uploadPath, dataList, fileSuffix, new HashMap<>(), checkMD5);
    }


    /**
     * 上传 （默认校验md5，如果文件过大，不建议使用该方法，选择可以将md5校验设置为false的方法）
     *
     * @param bucketName    桶
     * @param userName      用户名
     * @param password      密码，需要MD5加密
     * @param hosts         如:www.baidu.com
     * @param uploadDirPath 上传至又拍云的目录路径 (/test   /test/a/)
     * @param files         多文件
     * @param params        又拍云的可选参数(参考：{@link com.upyun.Params})
     * @return
     */
    public static List<String> parallelUpload(String bucketName, String userName, String password, String hosts, String uploadDirPath, List<File> files, Map<String, String> params) {
        return parallelUpload(UpYunConf.buildUpYunConf(bucketName, userName, password, hosts), uploadDirPath, files, params);
    }

    /**
     * 上传
     *
     * @param bucketName    桶
     * @param userName      用户名
     * @param password      密码，需要MD5加密
     * @param hosts         如:www.baidu.com
     * @param uploadDirPath 上传至又拍云的目录路径 (/test   /test/a/)
     * @param files         多文件
     * @return
     */
    public static List<String> parallelUpload(String bucketName, String userName, String password, String hosts, String uploadDirPath, List<File> files) {
        return parallelUpload(UpYunConf.buildUpYunConf(bucketName, userName, password, hosts), uploadDirPath, files, new HashMap<>());
    }


    /**
     * 上传
     *
     * @param upYunConf     又拍云配置
     * @param uploadDirPath 上传至又拍云的目录路径 (/test   /test/a/)
     * @param files         多文件
     * @return
     */
    public static List<String> parallelUpload(UpYunConf upYunConf, String uploadDirPath, List<File> files) {
        return parallelUpload(upYunConf, uploadDirPath, files, new HashMap<>());
    }

    /**
     * 上传
     *
     * @param upYunConf     又拍云配置
     * @param uploadDirPath 上传至又拍云的目录路径 (/test   /test/a/)
     * @param files         多文件
     * @param params        又拍云的可选参数(参考：{@link com.upyun.Params})
     * @return
     */
    public static List<String> parallelUpload(UpYunConf upYunConf, String uploadDirPath, List<File> files, Map<String, String> params) {
        return parallelUpload(upYunConf, uploadDirPath, files, params, false, true);
    }


    /**
     * 上传
     *
     * @param upYunConf             又拍云配置
     * @param uploadDirPath         上传至又拍云的目录路径 (/test   /test/a/)
     * @param files                 多文件
     * @param checkMD5              是否校验md5，如果文件过大，建议设置为false
     *                              // 设置待上传文件的 Content-MD5 值
     *                              // 如果又拍云服务端收到的文件MD5值与用户设置的不一致，将会报 406 NotAcceptable 错误
     * @param isUseGenerateFileName 是否使用算法生成文件名，TRUE 使用方法中的算法生成文件名；false 则使用文件本身的文件名
     * @return
     */
    public static List<String> parallelUpload(UpYunConf upYunConf, String uploadDirPath, List<File> files, boolean checkMD5, boolean isUseGenerateFileName) {
        return parallelUpload(upYunConf, uploadDirPath, files, new HashMap<>(), checkMD5, isUseGenerateFileName);
    }

    /**
     * 上传
     *
     * @param upYunConf             又拍云配置
     * @param uploadDirPath         上传至又拍云的目录路径 (/test   /test/a/)
     * @param files                 多文件
     * @param isUseGenerateFileName 是否使用算法生成文件名，TRUE 使用方法中的算法生成文件名；false 则使用文件本身的文件名
     * @return
     */
    public static List<String> parallelUpload(UpYunConf upYunConf, String uploadDirPath, List<File> files, boolean isUseGenerateFileName) {
        return parallelUpload(upYunConf, uploadDirPath, files, new HashMap<>(), false, isUseGenerateFileName);
    }


    /**
     * 上传
     *
     * @param upYunConf             又拍云配置
     * @param uploadDirPath         上传至又拍云的目录路径 (/test   /test/a/)
     * @param files                 多文件
     * @param params                又拍云的可选参数(参考：{@link com.upyun.Params})
     * @param checkMD5              是否校验md5，如果文件过大，建议设置为false
     *                              // 设置待上传文件的 Content-MD5 值
     *                              // 如果又拍云服务端收到的文件MD5值与用户设置的不一致，将会报 406 NotAcceptable 错误
     * @param isUseGenerateFileName 是否使用算法生成文件名，TRUE 使用方法中的算法生成文件名；false 则使用文件本身的文件名
     * @return
     */
    public static List<String> parallelUpload(UpYunConf upYunConf, String uploadDirPath, List<File> files, Map<String, String> params, boolean checkMD5, boolean isUseGenerateFileName) {
        return upload(upYunConf, params, ParallelUploader.class, (parallelUploader, map) -> {
            List<String> resultList = new ArrayList<>();
            try {
                check(isDirectory(uploadDirPath), "上传至又拍云的目录路径不合法,请校验");
                //设置上传进度监听
                parallelUploader.setOnProgressListener((index, total) -> log.debug("文件上传中=====:index::{},total::{}", index, index * 100 / total + "%"));
                //设置 MD5 校验
                parallelUploader.setCheckMD5(checkMD5);
                //利用线程池，多文件进行上传
                List<Callable<String>> tasks = new ArrayList<>();
                for (File file : files) {
                    Callable<String> callable = () -> {
                        String fileName = file.getName();
                        //如果使用方法中算法生成的文件名(注：文件名重复，会出现覆盖)
                        if (isUseGenerateFileName) {
                            fileName = StringUtils.join(IdUtils.fastSimpleUUID(), getFileSuffix(file));
                        }
                        String uploadPath = uploadDirPath.endsWith("/") ? StringUtils.join(uploadDirPath, fileName) : StringUtils.join(uploadDirPath, "/", fileName);
                        boolean upload = parallelUploader.upload(file.getPath(), uploadPath, params);
                        if (upload) {
                            return removeFutileSlash(StringUtils.join(getScheme(upYunConf.getAddress()), uploadPath));
                        }
                        return "";
                    };
                    tasks.add(callable);
                }
                //提交任务
                resultList.addAll(submit(tasks));
            } catch (Exception e) {
                log.error("文件上传失败:error:{}", e);
            }
            return resultList;
        });
    }


    /**
     * 下载(response.body() 包含文件流信息)
     *
     * @param upYunConf 又拍云配置
     * @param filePath  文件路径 /test/1484464385422077952.jpg
     * @return
     */
    public static byte[] read(UpYunConf upYunConf, String filePath) {
        RestManager restManager = new RestManager(upYunConf.getNameSpace(), upYunConf.getUserName(), upYunConf.getPassword());
        try {
            Response result = restManager.readFile(filePath);
            if (result.isSuccessful()) {
                return result.body().bytes();
            }
        } catch (Exception ex) {
            log.error("文件读取失败:{}", ex);
        }
        return null;
    }

    /**
     * 下载文件,返回未加如data:image/png;base64前缀的字符串(使用Base64编码方案将指定的字节数组编码为字符串)
     *
     * @param upYunConf 又拍云配置
     * @param filePath  又拍云文件路径 /test/1484464385422077952.jpg
     * @return
     */
    public static String readImage(UpYunConf upYunConf, String filePath) {
        byte[] read = read(upYunConf, filePath);
        return Base64.getEncoder().encodeToString(read);
    }

    /**
     * 下载文件,返回未加如data:image/png;base64前缀的字符串(使用Base64编码方案将指定的字节数组编码为字符串)
     *
     * @param spec 要解析为 URL的字符
     * @return
     */
    public static String readImage(String spec) {
        return Base64.getEncoder().encodeToString(IoUtil.readBytes(read(spec)));
    }


    /**
     * 下载文件，使用Base64编码方案将指定的字节数组编码为字符串，并在前面加上类似前缀data:image/png;base64,
     *
     * @param upYunConf
     * @param filePath  文件后缀名 如: jpg,不带.符号
     * @return
     */
    public static String base64StrToImage(UpYunConf upYunConf, String filePath) {
        return StringUtils.join("data:image/", getFileSuffix(filePath), ";base64,", readImage(upYunConf, filePath));
    }

    /**
     * 下载文件，使用Base64编码方案将指定的字节数组编码为字符串，并在前面加上类似前缀data:image/png;base64,
     *
     * @param spec       要解析为 URL的字符串
     * @param fileSuffix 文件后缀名 如: jpg,不带.符号
     * @return
     */
    public static String base64StrToImage(String spec, String fileSuffix) {
        //从要解析为 URL的字符串中获取文件后缀名,由于该文件设置了content-secret,所以获取文件后缀名，就会有pem!abc123，需要去掉content-secret(!abc123)
        return StringUtils.join("data:image/", StringUtils.isNotBlank(fileSuffix) ? fileSuffix : getFileSuffix(spec), ";base64,", readImage(spec));
    }

    /**
     * 下载文件，使用Base64编码方案将指定的字节数组编码为字符串，并在前面加上类似前缀data:image/png;base64,
     *
     * @param spec 要解析为 URL的字符串
     * @return
     */
    public static String base64StrToImage(String spec) {
        return base64StrToImage(spec, null);
    }

    /**
     * 下载文件
     *
     * @param spec 要解析为 URL的字符串
     * @return
     */
    public static InputStream read(String spec) {
        return FileUtil.getInputStream(spec);
    }


    /**
     * 获取文件后缀名(带.)
     *
     * @param file 文件
     * @return
     */
    private static String getFileSuffix(File file) {
        return "." + FileTypeUtils.getFileType(file);
    }

    /**
     * 获取文件后缀名(不带.)
     *
     * @param filePath 文件路径
     * @return
     */
    private static String getFileSuffix(String filePath) {
        String fileSuffix = FileTypeUtils.getFileType(filePath);
        //标识符可为半角字符：“!”，“-”，“_” 三种，可以在管理中进行更改。(默认是"!")
        if (fileSuffix.contains("!")) {
            fileSuffix = StringUtils.substring(fileSuffix, 0, fileSuffix.lastIndexOf("!"));
        } else if (fileSuffix.contains("-")) {
            fileSuffix = StringUtils.substring(fileSuffix, 0, fileSuffix.lastIndexOf("-"));
        } else if (fileSuffix.contains("_")) {
            fileSuffix = StringUtils.substring(fileSuffix, 0, fileSuffix.lastIndexOf("_"));
        }
        return fileSuffix;
    }


    /**
     * 是否是又拍云的路径
     * 如：
     * /test/           true
     * /test            true
     * /test/a.txt      false
     * /test/a.jpg      false
     *
     * @return
     */
    private static boolean isDirectory(String path) {
        //这里简单判断下,路径包含1个及以上的/，并且不带.
        return countShow(path, "/") >= 1 & !path.contains(".");
    }


    private static int countShow(String str,String target) {
        // 处理空值或空字符串情况
        if (str == null || target == null || str.isEmpty() || target.isEmpty()) {
            return 0;
        }

        int count = 0;
        int index = 0;
        int targetLength = target.length();

        // 循环查找子字符串出现的位置
        while ((index = str.indexOf(target, index)) != -1) {
            count++;
            // 移动索引，避免重叠计数（如"aaa"中查找"aa"会得到1次而非2次）
            index += targetLength;
        }

        return count;
    }
    /**
     * 判断是否为true，为false则抛异常
     *
     * @param flag
     * @param msg
     * @param <T>
     */
    public static <T> void check(boolean flag, String msg) {
        if (!flag) {
            throw new IllegalArgumentException(msg);
        }
    }


    /**
     * 是否需要重写URL
     *
     * @param url
     * @return
     */
    private static boolean isRewriteUrl(String url) {
        List<String> apiDomainList = Arrays.asList(RestManager.ED_AUTO, RestManager.ED_CNC, RestManager.ED_CTT, RestManager.ED_TELECOM);
        for (String apiDomain : apiDomainList) {
            if (url.contains(apiDomain)) {
                return true;
            }
        }
        return false;
    }


    /**
     * 将URL中无用的//替换成为/
     * 如：https://xx//test/20211215135525.png 替换成 https:/xx/test/20211215135525.png
     *
     * @param text
     * @return
     */
    private static String removeFutileSlash(String text) {
        //如果url中出现了一次以上的//,则拼接有问题
        if (countShow(text, "//") > 1) {
            //则将第二次及其后面的//替换成/
            //以双斜杠拆分
            List<String> strList = Splitter.on("//").splitToList(text);
            //再将如http://或者https://与后面的字符串用/拼接
            text = StringUtils.join(strList.stream().findFirst().get(), "//", strList.stream().skip(1).collect(Collectors.joining("/")));
        }
        return text;
    }


    /**
     * "http" or "https".
     *
     * @return
     */
    private static String getScheme(String hosts) {
        return hosts.startsWith("http") || hosts.startsWith("https") ? hosts : "http://" + hosts;
    }


    /**
     * 利用反射获取构造方法，并且实例化对象
     * 这里返回的实例对象(可能会是RestManager、SerialUploader、ParallelUploader等其中一个)
     *
     * @param clazz 类
     * @param args  参数
     * @param <T>
     * @return
     */
    private static <T> T getInstance(Class<T> clazz, Object... args) {
        //先从缓存中获取
        T t = (T) INSTANCE_CACHE.get(clazz);
        try {
            if (Objects.isNull(t)) {
                Constructor<T> constructor = ReflectUtil.getConstructor(clazz, String.class, String.class, String.class);
                T newInstance = constructor.newInstance(args);
                INSTANCE_CACHE.put(clazz, newInstance);
                return newInstance;
            }
        } catch (Exception e) {
            log.error("获取构造方法失败,error:{}", e);
        }
        return t;
    }


    /**
     * 线程池 提交任务
     *
     * @return
     */
    public static List<String> submit(List<Callable<String>> tasks) throws Exception {
        List<String> result = new ArrayList<>();

        StopWatch stopWatch = new StopWatch("耗时统计:");
        AtomicInteger taskNum = new AtomicInteger();

        int availableProcessors = Runtime.getRuntime().availableProcessors();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(availableProcessors,
                availableProcessors + 1,
                60,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(50),
                new ThreadPoolExecutor.CallerRunsPolicy());
        try {
            for (Callable<String> task : tasks) {
                stopWatch.start(StringUtils.join("task:", taskNum.addAndGet(1)));
                result.add(threadPoolExecutor.submit(task).get());
                stopWatch.stop();
            }
            return result;
        } finally {
            //打印耗时结果
            log.debug(stopWatch.prettyPrint(TimeUnit.MILLISECONDS));
            threadPoolExecutor.shutdown();
        }
    }

    public static void main(String[] args) throws URISyntaxException, MalformedURLException {
//        File f = new File("C:\\Users\\Administrator\\Pictures\\bizhi.jpg");
//        String s = UpyOssUtil.upload("img-yyy","yyy","QxE1OVpg5sQMo71UcZZiGocCyYMNpbAn","https://img.qytrades.com","/min/2025/09/10",FileUtil.readBytes(f),".jpg");
////        System.out.println(s);
        String subUrl = "min/2025/09/10/12efaf1175f24e73a8bb05ab0befcd63.jpg";
        String host = "http://img.qytrades.com";
        URI baseUri = new URI(host);
        URI fullUri = baseUri.resolve(subUrl);
        System.out.println(fullUri);
        System.out.println(fullUri.toString());
        System.out.println(fullUri.toURL());
        System.out.println(fullUri.toURL().toString());
        System.out.println(DateUtil.format(new Date(),"yyyy/MM/dd"));
    }
}
